{
 "cells": [
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-09T10:56:10.084819Z",
     "start_time": "2025-02-09T10:56:07.589089Z"
    }
   },
   "cell_type": "code",
   "source": [
    "# 导入库\n",
    "import matplotlib as mpl\n",
    "# Matplotlib 绘制的图形会直接嵌入到 Notebook 的输出单元格中，而不是弹出一个独立的窗口\n",
    "%matplotlib inline\n",
    "\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import sklearn\n",
    "import pandas as pd\n",
    "\n",
    "# os库提供了一种使用操作系统相关功能的便捷方式，允许与操作系统进行交互\n",
    "import os\n",
    "\n",
    "# sys库主要用于处理Python运行时的环境相关信息以及与解释器的交互\n",
    "import sys\n",
    "import time\n",
    "\n",
    "# tqdm库是一个快速，可扩展的Python进度条，可以在 Python 长循环中添加一个进度提示信息，用户只需要封装任意的迭代器 tqdm(iterator)，即可获得一个进度条显示迭代进度。\n",
    "from tqdm.auto import tqdm\n",
    "import torch\n",
    "\n",
    "# torch.nn是PyTorch中用于构建神经网络的模块\n",
    "import torch.nn as nn\n",
    "\n",
    "# torch.nn.functional是PyTorch中包含了神经网络的一些常用函数\n",
    "import torch.nn.functional as F\n",
    "\n",
    "# Python 中的一个属性，用于获取当前 Python 解释器的版本信息。它返回一个命名元组（named tuple）\n",
    "print(sys.version_info)\n",
    "\n",
    "# 遍历模块，打印模块名称和版本信息，快速检查当前环境中安装的某些常用 Python 库的版本信息\n",
    "for module in mpl, np, pd, sklearn, torch:\n",
    "    print(module.__name__, module.__version__)\n",
    "\n",
    "# 判断是否有 GPU，如果有，则使用 GPU 进行训练，否则使用 CPU 进行训练\n",
    "# torch.cuda.is_available()用于判断是否有GPU\n",
    "# torch.device(\"cuda:0\")创建一个 PyTorch 设备对象，表示使用第一个 GPU（索引为 0）\n",
    "# torch.device(\"cpu\")创建一个 PyTorch 设备对象，表示使用 CPU\n",
    "device = torch.device(\"cuda:0\") if torch.cuda.is_available() else torch.device(\"cpu\")\n",
    "print(device)\n",
    "\n",
    "seed = 42\n"
   ],
   "id": "209d74c6b96118f6",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sys.version_info(major=3, minor=12, micro=3, releaselevel='final', serial=0)\n",
      "matplotlib 3.10.0\n",
      "numpy 1.26.4\n",
      "pandas 2.2.3\n",
      "sklearn 1.6.0\n",
      "torch 2.3.1+cu121\n",
      "cuda:0\n"
     ]
    }
   ],
   "execution_count": 1
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "# 数据准备",
   "id": "5b5c5d1a1d368c06"
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-09T10:56:37.181848Z",
     "start_time": "2025-02-09T10:56:36.272240Z"
    }
   },
   "cell_type": "code",
   "source": [
    "# 导入所需的库：\n",
    "# datasets: PyTorch 提供的数据集工具。\n",
    "# ToTensor, Resize, Compose, ConvertImageDtype, Normalize: 图像预处理工具。\n",
    "# Path: 用于处理文件路径。\n",
    "from torchvision import datasets\n",
    "from torchvision.transforms import ToTensor, Resize, Compose, ConvertImageDtype, Normalize\n",
    "from pathlib import Path\n",
    "\n",
    "# 定义数据集的根目录为 ./archive/\n",
    "# 使用 Path 对象处理文件路径\n",
    "DATA_DIR = Path(\"./archive/\")\n",
    "\n",
    "\n",
    "# 定义一个名为 MonkeyDataset 的类，继承自 datasets.ImageFolder\n",
    "# ImageFolder 是 PyTorch 提供的用于加载图像文件夹数据集的类\n",
    "# 该类会将图像文件路径及标签组合成样本，并将样本组合成数据集\n",
    "# 该类会自动将图像文件转换为张量，并对张量进行预处理（如缩放、归一化等）\n",
    "class MonkeyDataset(datasets.ImageFolder):\n",
    "\n",
    "    #  定义类的构造函数，接受以下参数：\n",
    "    #  mode: 数据集模式（\"train\" 或 \"val\"）。\n",
    "    #  transform: 图像预处理操作，默认值为 None\n",
    "    # 初始化数据集类，根据模式选择数据子集\n",
    "    def __init__(self, mode, transform=None):\n",
    "\n",
    "        # 根据 mode 参数选择数据子集的根目录。\n",
    "        # 如果 mode 为 \"train\"，则使用 training 子目录。\n",
    "        # 如果 mode 为 \"val\"，则使用 validation 子目录。\n",
    "        # 否则，抛出错误。\n",
    "        # 原理: 使用 Path 对象拼接路径。\n",
    "        if mode == \"train\":\n",
    "            root = DATA_DIR / \"training\" / \"training\"\n",
    "        elif mode == \"val\":\n",
    "            root = DATA_DIR / \"validation\" / \"validation\"\n",
    "        else:\n",
    "            raise ValueError(\"mode should be one of the following: train, val, but got {}\".format(mode))\n",
    "\n",
    "        # 调用父类 ImageFolder 的构造函数，传入根目录和预处理操作\n",
    "        # 父类 ImageFolder 会自动加载图像数据并应用预处理操作\n",
    "        super().__init__(root, transform)\n",
    "\n",
    "        # 将数据集的标签（目标值）存储在 self.targets 中\n",
    "        # self.samples 是 ImageFolder 中存储的样本列表，每个样本是一个元组 (图像路径, 标签)\n",
    "        # 方便后续访问数据集的标签\n",
    "        self.targets = [s[1] for s in self.samples]\n",
    "\n",
    "\n",
    "# 定义图像预处理操作，包括：\n",
    "# Resize((img_h, img_w)): 将图像调整为 224x224 大小。\n",
    "# ToTensor(): 将图像转换为 PyTorch 张量。\n",
    "# Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]): 对图像进行归一化，使用 ImageNet 的均值和标准差。\n",
    "# ConvertImageDtype(torch.float): 将张量转换为 float 类型。\n",
    "# 原理: 使用 Compose 将多个预处理操作组合在一起。\n",
    "img_h, img_w = 224, 224\n",
    "transform = Compose([\n",
    "    Resize((img_h, img_w)),\n",
    "    ToTensor(),\n",
    "    Normalize([0.4363, 0.4328, 0.3291], [0.2129, 0.2075, 0.2037]),\n",
    "    ConvertImageDtype(torch.float),\n",
    "])\n",
    "\n",
    "# 创建训练集和验证集的实例\n",
    "# 使用 MonkeyDataset 类加载数据，并应用预处理操作\n",
    "train_ds = MonkeyDataset(\"train\", transform=transform)\n",
    "val_ds = MonkeyDataset(\"val\", transform=transform)\n",
    "\n",
    "# 打印训练集和验证集的样本数量\n",
    "print(\"load {} images from training dataset\".format(len(train_ds)))\n",
    "print(\"load {} images from validation dataset\".format(len(val_ds)))"
   ],
   "id": "93334ffafed9be5b",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "load 1097 images from training dataset\n",
      "load 272 images from validation dataset\n"
     ]
    }
   ],
   "execution_count": 2
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-09T10:56:38.357956Z",
     "start_time": "2025-02-09T10:56:38.354007Z"
    }
   },
   "cell_type": "code",
   "source": [
    "# 数据类别\n",
    "train_ds.classes"
   ],
   "id": "edcd8d2bcbd1fae8",
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['n0', 'n1', 'n2', 'n3', 'n4', 'n5', 'n6', 'n7', 'n8', 'n9']"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 3
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-09T10:56:43.851806Z",
     "start_time": "2025-02-09T10:56:43.846701Z"
    }
   },
   "cell_type": "code",
   "source": "train_ds.class_to_idx",
   "id": "c6f2e30f5b5be2ad",
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'n0': 0,\n",
       " 'n1': 1,\n",
       " 'n2': 2,\n",
       " 'n3': 3,\n",
       " 'n4': 4,\n",
       " 'n5': 5,\n",
       " 'n6': 6,\n",
       " 'n7': 7,\n",
       " 'n8': 8,\n",
       " 'n9': 9}"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 4
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-09T10:56:53.551652Z",
     "start_time": "2025-02-09T10:56:53.548744Z"
    }
   },
   "cell_type": "code",
   "source": [
    "import torch.nn as nn\n",
    "from torch.utils.data.dataloader import DataLoader\n",
    "\n",
    "batch_size = 64\n",
    "# 从数据集到dataloader，num_workers参数不能加，否则会报错\n",
    "# https://github.com/pytorch/pytorch/issues/59438\n",
    "train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True)\n",
    "val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False)"
   ],
   "id": "f2d1ef42ae6665",
   "outputs": [],
   "execution_count": 5
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-09T10:57:17.392471Z",
     "start_time": "2025-02-09T10:57:16.093508Z"
    }
   },
   "cell_type": "code",
   "source": [
    "for imgs, labels in train_loader:\n",
    "    print(imgs.shape)\n",
    "    print(labels.shape)\n",
    "    break"
   ],
   "id": "9449ba44b3f20d63",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "torch.Size([64, 3, 224, 224])\n",
      "torch.Size([64])\n"
     ]
    }
   ],
   "execution_count": 6
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "# 定义模型",
   "id": "898c3ddde26bf3df"
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": [
    "功能: 定义了一个基于 ResNet-50 的模型类 ResNet50，用于图像分类任务。\n",
    "\n",
    "原理:\n",
    "\n",
    "使用预训练的 ResNet-50 模型，加载 ImageNet 权重。\n",
    "\n",
    "冻结模型参数，保留预训练模型的特征提取能力。\n",
    "\n",
    "解冻部分参数，微调高层特征。\n",
    "\n",
    "替换最后一层全连接层，适应目标任务的输出类别数。\n",
    "\n",
    "作用:\n",
    "\n",
    "适用于图像分类任务（如 CIFAR-10、MNIST 等）。\n",
    "\n",
    "利用迁移学习提高模型性能。\n",
    "\n",
    "为什么这样做:\n",
    "\n",
    "使用预训练模型可以加速模型开发，并提高性能。\n",
    "\n",
    "冻结参数可以减少训练时间和计算量。\n",
    "\n",
    "部分解冻参数可以在保留预训练模型特征提取能力的同时，微调高层特征。\n",
    "\n",
    "替换最后一层全连接层，适应目标任务的输出类别数。"
   ],
   "id": "d5a11f783f5602aa"
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-09T12:13:25.178517Z",
     "start_time": "2025-02-09T12:13:21.362780Z"
    }
   },
   "cell_type": "code",
   "source": [
    "#  从 torchvision.models 中导入 resnet50 模型\n",
    "# torchvision.models 提供了预定义的经典模型\n",
    "# 使用预训练模型可以加速模型开发，并利用迁移学习提高性能\n",
    "from torchvision.models import resnet50\n",
    "\n",
    "\n",
    "# 定义一个名为 ResNet50 的类，继承自 nn.Module\n",
    "class ResNet50(nn.Module):\n",
    "\n",
    "    # 定义类的构造函数，接受以下参数\n",
    "    # num_classes: 输出类别数，默认值为 10\n",
    "    # frozen: 是否冻结模型参数，默认值为 True\n",
    "    def __init__(self, num_classes=10, frozen=True):\n",
    "        super().__init__()\n",
    "\n",
    "        # 加载预训练的 ResNet-50 模型，使用 IMAGENET1K_V2 权重\n",
    "        # resnet50 是 PyTorch 提供的 ResNet-50 模型，weights=\"IMAGENET1K_V2\" 表示加载在 ImageNet 数据集上预训练的权重\n",
    "        # 使用预训练模型可以加速模型开发，并利用迁移学习提高性能\n",
    "        self.model = resnet50(weights=\"IMAGENET1K_V2\", )\n",
    "\n",
    "        # 如果 frozen 为 True，则冻结模型的所有参数\n",
    "        # 将 requires_grad 设置为 False，使参数在训练过程中不更新\n",
    "        # 冻结参数可以保留预训练模型的特征提取能力，同时减少训练时间和计算量。\n",
    "        if frozen:\n",
    "            for param in self.model.parameters():\n",
    "                param.requires_grad = False\n",
    "\n",
    "        # 解冻 layer4.2.conv3.weight 参数，使其在训练过程中更新\n",
    "        # 通过 named_parameters() 获取参数名称和值，并根据名称解冻特定参数\n",
    "        #  部分解冻参数可以在保留预训练模型特征提取能力的同时，微调高层特征。\n",
    "        for name, param in self.model.named_parameters():\n",
    "            if name == \"layer4.2.conv3.weight\":\n",
    "                param.requires_grad = True\n",
    "\n",
    "        # 替换 ResNet-50 的最后一层全连接层，使其输出维度为 num_classes\n",
    "        # self.model.fc.in_features 是原始全连接层的输入维度，num_classes 是目标输出维度 \n",
    "        self.model.fc = nn.Linear(self.model.fc.in_features, num_classes)\n",
    "\n",
    "    def forward(self, x):\n",
    "        return self.model(x)\n",
    "\n",
    "\n",
    "# 遍历模型的参数，打印每个参数的名称和数量\n",
    "# np.prod(value.shape): 计算参数的个数（形状的乘积）\n",
    "for idx, (key, value) in enumerate(ResNet50().named_parameters()):\n",
    "    print(f\"{key:^40}paramerters num: {np.prod(value.shape)}\")"
   ],
   "id": "b2d38cb606810ab4",
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Downloading: \"https://download.pytorch.org/models/resnet50-11ad3fa6.pth\" to C:\\Users\\35493/.cache\\torch\\hub\\checkpoints\\resnet50-11ad3fa6.pth\n",
      "100%|██████████| 97.8M/97.8M [00:03<00:00, 30.2MB/s]\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "           model.conv1.weight           paramerters num: 9408\n",
      "            model.bn1.weight            paramerters num: 64\n",
      "             model.bn1.bias             paramerters num: 64\n",
      "      model.layer1.0.conv1.weight       paramerters num: 4096\n",
      "       model.layer1.0.bn1.weight        paramerters num: 64\n",
      "        model.layer1.0.bn1.bias         paramerters num: 64\n",
      "      model.layer1.0.conv2.weight       paramerters num: 36864\n",
      "       model.layer1.0.bn2.weight        paramerters num: 64\n",
      "        model.layer1.0.bn2.bias         paramerters num: 64\n",
      "      model.layer1.0.conv3.weight       paramerters num: 16384\n",
      "       model.layer1.0.bn3.weight        paramerters num: 256\n",
      "        model.layer1.0.bn3.bias         paramerters num: 256\n",
      "   model.layer1.0.downsample.0.weight   paramerters num: 16384\n",
      "   model.layer1.0.downsample.1.weight   paramerters num: 256\n",
      "    model.layer1.0.downsample.1.bias    paramerters num: 256\n",
      "      model.layer1.1.conv1.weight       paramerters num: 16384\n",
      "       model.layer1.1.bn1.weight        paramerters num: 64\n",
      "        model.layer1.1.bn1.bias         paramerters num: 64\n",
      "      model.layer1.1.conv2.weight       paramerters num: 36864\n",
      "       model.layer1.1.bn2.weight        paramerters num: 64\n",
      "        model.layer1.1.bn2.bias         paramerters num: 64\n",
      "      model.layer1.1.conv3.weight       paramerters num: 16384\n",
      "       model.layer1.1.bn3.weight        paramerters num: 256\n",
      "        model.layer1.1.bn3.bias         paramerters num: 256\n",
      "      model.layer1.2.conv1.weight       paramerters num: 16384\n",
      "       model.layer1.2.bn1.weight        paramerters num: 64\n",
      "        model.layer1.2.bn1.bias         paramerters num: 64\n",
      "      model.layer1.2.conv2.weight       paramerters num: 36864\n",
      "       model.layer1.2.bn2.weight        paramerters num: 64\n",
      "        model.layer1.2.bn2.bias         paramerters num: 64\n",
      "      model.layer1.2.conv3.weight       paramerters num: 16384\n",
      "       model.layer1.2.bn3.weight        paramerters num: 256\n",
      "        model.layer1.2.bn3.bias         paramerters num: 256\n",
      "      model.layer2.0.conv1.weight       paramerters num: 32768\n",
      "       model.layer2.0.bn1.weight        paramerters num: 128\n",
      "        model.layer2.0.bn1.bias         paramerters num: 128\n",
      "      model.layer2.0.conv2.weight       paramerters num: 147456\n",
      "       model.layer2.0.bn2.weight        paramerters num: 128\n",
      "        model.layer2.0.bn2.bias         paramerters num: 128\n",
      "      model.layer2.0.conv3.weight       paramerters num: 65536\n",
      "       model.layer2.0.bn3.weight        paramerters num: 512\n",
      "        model.layer2.0.bn3.bias         paramerters num: 512\n",
      "   model.layer2.0.downsample.0.weight   paramerters num: 131072\n",
      "   model.layer2.0.downsample.1.weight   paramerters num: 512\n",
      "    model.layer2.0.downsample.1.bias    paramerters num: 512\n",
      "      model.layer2.1.conv1.weight       paramerters num: 65536\n",
      "       model.layer2.1.bn1.weight        paramerters num: 128\n",
      "        model.layer2.1.bn1.bias         paramerters num: 128\n",
      "      model.layer2.1.conv2.weight       paramerters num: 147456\n",
      "       model.layer2.1.bn2.weight        paramerters num: 128\n",
      "        model.layer2.1.bn2.bias         paramerters num: 128\n",
      "      model.layer2.1.conv3.weight       paramerters num: 65536\n",
      "       model.layer2.1.bn3.weight        paramerters num: 512\n",
      "        model.layer2.1.bn3.bias         paramerters num: 512\n",
      "      model.layer2.2.conv1.weight       paramerters num: 65536\n",
      "       model.layer2.2.bn1.weight        paramerters num: 128\n",
      "        model.layer2.2.bn1.bias         paramerters num: 128\n",
      "      model.layer2.2.conv2.weight       paramerters num: 147456\n",
      "       model.layer2.2.bn2.weight        paramerters num: 128\n",
      "        model.layer2.2.bn2.bias         paramerters num: 128\n",
      "      model.layer2.2.conv3.weight       paramerters num: 65536\n",
      "       model.layer2.2.bn3.weight        paramerters num: 512\n",
      "        model.layer2.2.bn3.bias         paramerters num: 512\n",
      "      model.layer2.3.conv1.weight       paramerters num: 65536\n",
      "       model.layer2.3.bn1.weight        paramerters num: 128\n",
      "        model.layer2.3.bn1.bias         paramerters num: 128\n",
      "      model.layer2.3.conv2.weight       paramerters num: 147456\n",
      "       model.layer2.3.bn2.weight        paramerters num: 128\n",
      "        model.layer2.3.bn2.bias         paramerters num: 128\n",
      "      model.layer2.3.conv3.weight       paramerters num: 65536\n",
      "       model.layer2.3.bn3.weight        paramerters num: 512\n",
      "        model.layer2.3.bn3.bias         paramerters num: 512\n",
      "      model.layer3.0.conv1.weight       paramerters num: 131072\n",
      "       model.layer3.0.bn1.weight        paramerters num: 256\n",
      "        model.layer3.0.bn1.bias         paramerters num: 256\n",
      "      model.layer3.0.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.0.bn2.weight        paramerters num: 256\n",
      "        model.layer3.0.bn2.bias         paramerters num: 256\n",
      "      model.layer3.0.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.0.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.0.bn3.bias         paramerters num: 1024\n",
      "   model.layer3.0.downsample.0.weight   paramerters num: 524288\n",
      "   model.layer3.0.downsample.1.weight   paramerters num: 1024\n",
      "    model.layer3.0.downsample.1.bias    paramerters num: 1024\n",
      "      model.layer3.1.conv1.weight       paramerters num: 262144\n",
      "       model.layer3.1.bn1.weight        paramerters num: 256\n",
      "        model.layer3.1.bn1.bias         paramerters num: 256\n",
      "      model.layer3.1.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.1.bn2.weight        paramerters num: 256\n",
      "        model.layer3.1.bn2.bias         paramerters num: 256\n",
      "      model.layer3.1.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.1.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.1.bn3.bias         paramerters num: 1024\n",
      "      model.layer3.2.conv1.weight       paramerters num: 262144\n",
      "       model.layer3.2.bn1.weight        paramerters num: 256\n",
      "        model.layer3.2.bn1.bias         paramerters num: 256\n",
      "      model.layer3.2.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.2.bn2.weight        paramerters num: 256\n",
      "        model.layer3.2.bn2.bias         paramerters num: 256\n",
      "      model.layer3.2.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.2.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.2.bn3.bias         paramerters num: 1024\n",
      "      model.layer3.3.conv1.weight       paramerters num: 262144\n",
      "       model.layer3.3.bn1.weight        paramerters num: 256\n",
      "        model.layer3.3.bn1.bias         paramerters num: 256\n",
      "      model.layer3.3.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.3.bn2.weight        paramerters num: 256\n",
      "        model.layer3.3.bn2.bias         paramerters num: 256\n",
      "      model.layer3.3.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.3.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.3.bn3.bias         paramerters num: 1024\n",
      "      model.layer3.4.conv1.weight       paramerters num: 262144\n",
      "       model.layer3.4.bn1.weight        paramerters num: 256\n",
      "        model.layer3.4.bn1.bias         paramerters num: 256\n",
      "      model.layer3.4.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.4.bn2.weight        paramerters num: 256\n",
      "        model.layer3.4.bn2.bias         paramerters num: 256\n",
      "      model.layer3.4.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.4.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.4.bn3.bias         paramerters num: 1024\n",
      "      model.layer3.5.conv1.weight       paramerters num: 262144\n",
      "       model.layer3.5.bn1.weight        paramerters num: 256\n",
      "        model.layer3.5.bn1.bias         paramerters num: 256\n",
      "      model.layer3.5.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.5.bn2.weight        paramerters num: 256\n",
      "        model.layer3.5.bn2.bias         paramerters num: 256\n",
      "      model.layer3.5.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.5.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.5.bn3.bias         paramerters num: 1024\n",
      "      model.layer4.0.conv1.weight       paramerters num: 524288\n",
      "       model.layer4.0.bn1.weight        paramerters num: 512\n",
      "        model.layer4.0.bn1.bias         paramerters num: 512\n",
      "      model.layer4.0.conv2.weight       paramerters num: 2359296\n",
      "       model.layer4.0.bn2.weight        paramerters num: 512\n",
      "        model.layer4.0.bn2.bias         paramerters num: 512\n",
      "      model.layer4.0.conv3.weight       paramerters num: 1048576\n",
      "       model.layer4.0.bn3.weight        paramerters num: 2048\n",
      "        model.layer4.0.bn3.bias         paramerters num: 2048\n",
      "   model.layer4.0.downsample.0.weight   paramerters num: 2097152\n",
      "   model.layer4.0.downsample.1.weight   paramerters num: 2048\n",
      "    model.layer4.0.downsample.1.bias    paramerters num: 2048\n",
      "      model.layer4.1.conv1.weight       paramerters num: 1048576\n",
      "       model.layer4.1.bn1.weight        paramerters num: 512\n",
      "        model.layer4.1.bn1.bias         paramerters num: 512\n",
      "      model.layer4.1.conv2.weight       paramerters num: 2359296\n",
      "       model.layer4.1.bn2.weight        paramerters num: 512\n",
      "        model.layer4.1.bn2.bias         paramerters num: 512\n",
      "      model.layer4.1.conv3.weight       paramerters num: 1048576\n",
      "       model.layer4.1.bn3.weight        paramerters num: 2048\n",
      "        model.layer4.1.bn3.bias         paramerters num: 2048\n",
      "      model.layer4.2.conv1.weight       paramerters num: 1048576\n",
      "       model.layer4.2.bn1.weight        paramerters num: 512\n",
      "        model.layer4.2.bn1.bias         paramerters num: 512\n",
      "      model.layer4.2.conv2.weight       paramerters num: 2359296\n",
      "       model.layer4.2.bn2.weight        paramerters num: 512\n",
      "        model.layer4.2.bn2.bias         paramerters num: 512\n",
      "      model.layer4.2.conv3.weight       paramerters num: 1048576\n",
      "       model.layer4.2.bn3.weight        paramerters num: 2048\n",
      "        model.layer4.2.bn3.bias         paramerters num: 2048\n",
      "            model.fc.weight             paramerters num: 20480\n",
      "             model.fc.bias              paramerters num: 10\n"
     ]
    }
   ],
   "execution_count": 7
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-09T12:13:52.582536Z",
     "start_time": "2025-02-09T12:13:52.414406Z"
    }
   },
   "cell_type": "code",
   "source": [
    "model = ResNet50(num_classes=10, frozen=True)\n",
    "\n",
    "\n",
    "def count_parameters(model):  #计算模型总参数量\n",
    "    return sum(p.numel() for p in model.parameters() if p.requires_grad)\n",
    "\n",
    "\n",
    "count_parameters(model)"
   ],
   "id": "7e6f1280720462cc",
   "outputs": [
    {
     "data": {
      "text/plain": [
       "1069066"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 8
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-09T12:14:01.172530Z",
     "start_time": "2025-02-09T12:14:01.168089Z"
    }
   },
   "cell_type": "code",
   "source": "model",
   "id": "b64085a254db5a29",
   "outputs": [
    {
     "data": {
      "text/plain": [
       "ResNet50(\n",
       "  (model): ResNet(\n",
       "    (conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)\n",
       "    (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "    (relu): ReLU(inplace=True)\n",
       "    (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)\n",
       "    (layer1): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "          (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (layer2): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)\n",
       "          (1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (3): Bottleneck(\n",
       "        (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (layer3): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(512, 1024, kernel_size=(1, 1), stride=(2, 2), bias=False)\n",
       "          (1): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (3): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (4): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (5): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (layer4): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(1024, 2048, kernel_size=(1, 1), stride=(2, 2), bias=False)\n",
       "          (1): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (avgpool): AdaptiveAvgPool2d(output_size=(1, 1))\n",
       "    (fc): Linear(in_features=2048, out_features=10, bias=True)\n",
       "  )\n",
       ")"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 9
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-09T12:14:31.924463Z",
     "start_time": "2025-02-09T12:14:31.921277Z"
    }
   },
   "cell_type": "code",
   "source": [
    "total_params = sum(p.numel() for p in model.parameters())\n",
    "print(f\"Total trainable parameters: {total_params}\")"
   ],
   "id": "36ae000b48eec985",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Total trainable parameters: 23528522\n"
     ]
    }
   ],
   "execution_count": 10
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-09T12:15:21.106082Z",
     "start_time": "2025-02-09T12:15:21.096271Z"
    }
   },
   "cell_type": "code",
   "source": [
    "# AdaptiveAvgPool2d: 自适应平均池化层\n",
    "# 作用: 输入一个张量，输出一个定长的张量\n",
    "# 原理: 输入一个张量，通过池化操作，输出一个定长的张量\n",
    "m = nn.AdaptiveAvgPool2d(output_size=(1, 1))\n",
    "input = torch.randn(1, 2048, 9, 9)\n",
    "output = m(input)\n",
    "output.shape"
   ],
   "id": "9c4a4d84e24fa86a",
   "outputs": [
    {
     "data": {
      "text/plain": [
       "torch.Size([1, 2048, 1, 1])"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 11
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "# 训练模型",
   "id": "619b0b5cce8a5a9d"
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-09T12:15:43.899861Z",
     "start_time": "2025-02-09T12:15:43.760606Z"
    }
   },
   "cell_type": "code",
   "source": [
    "from sklearn.metrics import accuracy_score\n",
    "\n",
    "\n",
    "@torch.no_grad()\n",
    "def evaluating(model, dataloader, loss_fct):\n",
    "    loss_list = []\n",
    "    pred_list = []\n",
    "    label_list = []\n",
    "    for datas, labels in dataloader:\n",
    "        datas = datas.to(device)\n",
    "        labels = labels.to(device)\n",
    "        # 前向计算\n",
    "        logits = model(datas)\n",
    "        loss = loss_fct(logits, labels)  # 验证集损失\n",
    "        loss_list.append(loss.item())\n",
    "\n",
    "        preds = logits.argmax(axis=-1)  # 验证集预测\n",
    "        pred_list.extend(preds.cpu().numpy().tolist())\n",
    "        label_list.extend(labels.cpu().numpy().tolist())\n",
    "\n",
    "    acc = accuracy_score(label_list, pred_list)\n",
    "    return np.mean(loss_list), acc\n"
   ],
   "id": "838543f291afe772",
   "outputs": [],
   "execution_count": 12
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-09T12:15:52.300727Z",
     "start_time": "2025-02-09T12:15:49.036150Z"
    }
   },
   "cell_type": "code",
   "source": [
    "from torch.utils.tensorboard import SummaryWriter\n",
    "\n",
    "\n",
    "class TensorBoardCallback:\n",
    "    def __init__(self, log_dir, flush_secs=10):\n",
    "        \"\"\"\n",
    "        Args:\n",
    "            log_dir (str): dir to write log.\n",
    "            flush_secs (int, optional): write to dsk each flush_secs seconds. Defaults to 10.\n",
    "        \"\"\"\n",
    "        self.writer = SummaryWriter(log_dir=log_dir, flush_secs=flush_secs)\n",
    "\n",
    "    def draw_model(self, model, input_shape):\n",
    "        self.writer.add_graph(model, input_to_model=torch.randn(input_shape))\n",
    "\n",
    "    def add_loss_scalars(self, step, loss, val_loss):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/loss\",\n",
    "            tag_scalar_dict={\"loss\": loss, \"val_loss\": val_loss},\n",
    "            global_step=step,\n",
    "        )\n",
    "\n",
    "    def add_acc_scalars(self, step, acc, val_acc):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/accuracy\",\n",
    "            tag_scalar_dict={\"accuracy\": acc, \"val_accuracy\": val_acc},\n",
    "            global_step=step,\n",
    "        )\n",
    "\n",
    "    def add_lr_scalars(self, step, learning_rate):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/learning_rate\",\n",
    "            tag_scalar_dict={\"learning_rate\": learning_rate},\n",
    "            global_step=step,\n",
    "\n",
    "        )\n",
    "\n",
    "    def __call__(self, step, **kwargs):\n",
    "        # add loss\n",
    "        loss = kwargs.pop(\"loss\", None)\n",
    "        val_loss = kwargs.pop(\"val_loss\", None)\n",
    "        if loss is not None and val_loss is not None:\n",
    "            self.add_loss_scalars(step, loss, val_loss)\n",
    "        # add acc\n",
    "        acc = kwargs.pop(\"acc\", None)\n",
    "        val_acc = kwargs.pop(\"val_acc\", None)\n",
    "        if acc is not None and val_acc is not None:\n",
    "            self.add_acc_scalars(step, acc, val_acc)\n",
    "        # add lr\n",
    "        learning_rate = kwargs.pop(\"lr\", None)\n",
    "        if learning_rate is not None:\n",
    "            self.add_lr_scalars(step, learning_rate)\n"
   ],
   "id": "90b6828d98e1892a",
   "outputs": [],
   "execution_count": 13
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-09T12:15:53.378068Z",
     "start_time": "2025-02-09T12:15:53.374670Z"
    }
   },
   "cell_type": "code",
   "source": [
    "class SaveCheckpointsCallback:\n",
    "    def __init__(self, save_dir, save_step=5000, save_best_only=True):\n",
    "        \"\"\"\n",
    "        Save checkpoints each save_epoch epoch. \n",
    "        We save checkpoint by epoch in this implementation.\n",
    "        Usually, training scripts with pytorch evaluating model and save checkpoint by step.\n",
    "\n",
    "        Args:\n",
    "            save_dir (str): dir to save checkpoint\n",
    "            save_epoch (int, optional): the frequency to save checkpoint. Defaults to 1.\n",
    "            save_best_only (bool, optional): If True, only save the best model or save each model at every epoch.\n",
    "        \"\"\"\n",
    "        self.save_dir = save_dir\n",
    "        self.save_step = save_step\n",
    "        self.save_best_only = save_best_only\n",
    "        self.best_metrics = -1\n",
    "\n",
    "        # mkdir\n",
    "        if not os.path.exists(self.save_dir):\n",
    "            os.mkdir(self.save_dir)\n",
    "\n",
    "    def __call__(self, step, state_dict, metric=None):\n",
    "        if step % self.save_step > 0:\n",
    "            return\n",
    "\n",
    "        if self.save_best_only:\n",
    "            assert metric is not None\n",
    "            if metric >= self.best_metrics:\n",
    "                # save checkpoints\n",
    "                torch.save(state_dict, os.path.join(self.save_dir, \"best.ckpt\"))\n",
    "                # update best metrics\n",
    "                self.best_metrics = metric\n",
    "        else:\n",
    "            torch.save(state_dict, os.path.join(self.save_dir, f\"{step}.ckpt\"))\n",
    "\n"
   ],
   "id": "dcb6f89aae0e047d",
   "outputs": [],
   "execution_count": 14
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-09T12:15:57.463646Z",
     "start_time": "2025-02-09T12:15:57.459644Z"
    }
   },
   "cell_type": "code",
   "source": [
    "class EarlyStopCallback:\n",
    "    def __init__(self, patience=5, min_delta=0.01):\n",
    "        \"\"\"\n",
    "\n",
    "        Args:\n",
    "            patience (int, optional): Number of epochs with no improvement after which training will be stopped.. Defaults to 5.\n",
    "            min_delta (float, optional): Minimum change in the monitored quantity to qualify as an improvement, i.e. an absolute \n",
    "                change of less than min_delta, will count as no improvement. Defaults to 0.01.\n",
    "        \"\"\"\n",
    "        self.patience = patience\n",
    "        self.min_delta = min_delta\n",
    "        self.best_metric = -1\n",
    "        self.counter = 0\n",
    "\n",
    "    def __call__(self, metric):\n",
    "        if metric >= self.best_metric + self.min_delta:\n",
    "            # update best metric\n",
    "            self.best_metric = metric\n",
    "            # reset counter \n",
    "            self.counter = 0\n",
    "        else:\n",
    "            self.counter += 1\n",
    "\n",
    "    @property\n",
    "    def early_stop(self):\n",
    "        return self.counter >= self.patience\n"
   ],
   "id": "7b4a99177b845623",
   "outputs": [],
   "execution_count": 15
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-09T12:16:40.517234Z",
     "start_time": "2025-02-09T12:16:40.319669Z"
    }
   },
   "cell_type": "code",
   "source": [
    "# 训练\n",
    "def training(\n",
    "        model,\n",
    "        train_loader,\n",
    "        val_loader,\n",
    "        epoch,\n",
    "        loss_fct,\n",
    "        optimizer,\n",
    "        tensorboard_callback=None,\n",
    "        save_ckpt_callback=None,\n",
    "        early_stop_callback=None,\n",
    "        eval_step=500,\n",
    "):\n",
    "    record_dict = {\n",
    "        \"train\": [],\n",
    "        \"val\": []\n",
    "    }\n",
    "\n",
    "    global_step = 0\n",
    "    model.train()\n",
    "    with tqdm(total=epoch * len(train_loader)) as pbar:\n",
    "        for epoch_id in range(epoch):\n",
    "            # training\n",
    "            for datas, labels in train_loader:\n",
    "                datas = datas.to(device)\n",
    "                labels = labels.to(device)\n",
    "                # 梯度清空\n",
    "                optimizer.zero_grad()\n",
    "                # 模型前向计算\n",
    "                logits = model(datas)\n",
    "                # 计算损失\n",
    "                loss = loss_fct(logits, labels)\n",
    "                # 梯度回传\n",
    "                loss.backward()\n",
    "                # 调整优化器，包括学习率的变动等\n",
    "                optimizer.step()\n",
    "                preds = logits.argmax(axis=-1)\n",
    "\n",
    "                acc = accuracy_score(labels.cpu().numpy(), preds.cpu().numpy())\n",
    "                loss = loss.cpu().item()\n",
    "                # record\n",
    "\n",
    "                record_dict[\"train\"].append({\n",
    "                    \"loss\": loss, \"acc\": acc, \"step\": global_step\n",
    "                })\n",
    "\n",
    "                # evaluating\n",
    "                if global_step % eval_step == 0:\n",
    "                    model.eval()\n",
    "                    val_loss, val_acc = evaluating(model, val_loader, loss_fct)\n",
    "                    record_dict[\"val\"].append({\n",
    "                        \"loss\": val_loss, \"acc\": val_acc, \"step\": global_step\n",
    "                    })\n",
    "                    model.train()\n",
    "\n",
    "                    # 1. 使用 tensorboard 可视化\n",
    "                    if tensorboard_callback is not None:\n",
    "                        tensorboard_callback(\n",
    "                            global_step,\n",
    "                            loss=loss, val_loss=val_loss,\n",
    "                            acc=acc, val_acc=val_acc,\n",
    "                            lr=optimizer.param_groups[0][\"lr\"],\n",
    "                        )\n",
    "\n",
    "                    # 2. 保存模型权重 save model checkpoint\n",
    "                    if save_ckpt_callback is not None:\n",
    "                        save_ckpt_callback(global_step, model.state_dict(), metric=val_acc)\n",
    "\n",
    "                    # 3. 早停 Early Stop\n",
    "                    if early_stop_callback is not None:\n",
    "                        early_stop_callback(val_acc)\n",
    "                        if early_stop_callback.early_stop:\n",
    "                            print(f\"Early stop at epoch {epoch_id} / global_step {global_step}\")\n",
    "                            return record_dict\n",
    "\n",
    "                # udate step\n",
    "                global_step += 1\n",
    "                pbar.update(1)\n",
    "                pbar.set_postfix({\"epoch\": epoch_id})\n",
    "\n",
    "    return record_dict\n",
    "\n",
    "\n",
    "epoch = 20\n",
    "\n",
    "model = ResNet50(num_classes=10)\n",
    "\n",
    "# 1. 定义损失函数 采用交叉熵损失\n",
    "loss_fct = nn.CrossEntropyLoss()\n",
    "# 2. 定义优化器 采用 sgd\n",
    "# Optimizers specified in the torch.optim package\n",
    "optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.0)\n",
    "\n",
    "# 1. tensorboard 可视化\n",
    "# if not os.path.exists(\"runs\"):\n",
    "#     os.mkdir(\"runs\")\n",
    "# tensorboard_callback = TensorBoardCallback(\"runs/monkeys-resnet50\")\n",
    "# tensorboard_callback.draw_model(model, [1, 3, img_h, img_w])\n",
    "# 2. save best\n",
    "if not os.path.exists(\"checkpoints\"):\n",
    "    os.makedirs(\"checkpoints\")\n",
    "save_ckpt_callback = SaveCheckpointsCallback(\"checkpoints/monkeys-resnet50\", save_step=len(train_loader),\n",
    "                                             save_best_only=True)\n",
    "# 3. early stop\n",
    "early_stop_callback = EarlyStopCallback(patience=5)"
   ],
   "id": "73fcd355d03120ce",
   "outputs": [],
   "execution_count": 16
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-09T12:20:45.949495Z",
     "start_time": "2025-02-09T12:16:45.908284Z"
    }
   },
   "cell_type": "code",
   "source": [
    "model = model.to(device)\n",
    "record = training(\n",
    "    model,\n",
    "    train_loader,\n",
    "    val_loader,\n",
    "    epoch,\n",
    "    loss_fct,\n",
    "    optimizer,\n",
    "    tensorboard_callback=None,\n",
    "    save_ckpt_callback=save_ckpt_callback,\n",
    "    early_stop_callback=early_stop_callback,\n",
    "    eval_step=len(train_loader)\n",
    ")"
   ],
   "id": "e5ecf3be67b9af96",
   "outputs": [
    {
     "data": {
      "text/plain": [
       "  0%|          | 0/360 [00:00<?, ?it/s]"
      ],
      "application/vnd.jupyter.widget-view+json": {
       "version_major": 2,
       "version_minor": 0,
       "model_id": "64bc12ce143c48a49f786b947fe8e390"
      }
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Early stop at epoch 10 / global_step 180\n"
     ]
    }
   ],
   "execution_count": 17
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-09T12:23:33.649192Z",
     "start_time": "2025-02-09T12:23:33.115023Z"
    }
   },
   "cell_type": "code",
   "source": [
    "# 从 torchviz 库中导入 make_dot 函数\n",
    "# make_dot 用于生成 PyTorch 模型的计算图\n",
    "from torchviz import make_dot\n",
    "\n",
    "# 创建一个虚拟输入张量，形状为 (1, 1, 28, 28)\n",
    "# 需要一个输入张量来运行模型并生成计算图\n",
    "dummy_input = torch.randn(1, 3, 224, 224).to(device)\n",
    "\n",
    "# 将虚拟输入张量传递给模型，得到输出\n",
    "# 计算图是基于模型的前向传播过程生成的，因此需要运行模型\n",
    "output = model(dummy_input).to(device)\n",
    "\n",
    "# 生成模型的计算图\n",
    "# output: 模型的输出张量\n",
    "# params: 模型的参数（通过 model.named_parameters() 获取）\n",
    "# make_dot 使用输出张量和模型参数来构建计算图\n",
    "dot = make_dot(output, params=dict(model.named_parameters()))\n",
    "\n",
    "# 生成一个名为 model_CNN.png 的文件\n",
    "dot.render(\"model_architecture\", format=\"png\")"
   ],
   "id": "18f55cb8497de54c",
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'model_architecture.png'"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 18
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-09T12:23:45.410074Z",
     "start_time": "2025-02-09T12:23:45.280165Z"
    }
   },
   "cell_type": "code",
   "source": [
    "#画线要注意的是损失是不一定在零到1之间的\n",
    "def plot_learning_curves(record_dict, sample_step=500):\n",
    "    # build DataFrame\n",
    "    train_df = pd.DataFrame(record_dict[\"train\"]).set_index(\"step\").iloc[::sample_step]\n",
    "    val_df = pd.DataFrame(record_dict[\"val\"]).set_index(\"step\")\n",
    "\n",
    "    # plot\n",
    "    fig_num = len(train_df.columns)\n",
    "    fig, axs = plt.subplots(1, fig_num, figsize=(5 * fig_num, 5))\n",
    "    for idx, item in enumerate(train_df.columns):\n",
    "        axs[idx].plot(train_df.index, train_df[item], label=f\"train_{item}\")\n",
    "        axs[idx].plot(val_df.index, val_df[item], label=f\"val_{item}\")\n",
    "        axs[idx].grid()\n",
    "        axs[idx].legend()\n",
    "        # axs[idx].set_xticks(range(0, train_df.index[-1], 5000))\n",
    "        # axs[idx].set_xticklabels(map(lambda x: f\"{int(x/1000)}k\", range(0, train_df.index[-1], 5000)))\n",
    "        axs[idx].set_xlabel(\"step\")\n",
    "\n",
    "    plt.show()\n",
    "\n",
    "\n",
    "plot_learning_curves(record, sample_step=10)  #横坐标是 steps"
   ],
   "id": "d7239b0d68dadd4c",
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Figure size 1000x500 with 2 Axes>"
      ],
      "image/png": "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"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "execution_count": 19
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "# 评估模型",
   "id": "4df2602ea8ad0875"
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-09T12:24:08.871143Z",
     "start_time": "2025-02-09T12:24:03.742141Z"
    }
   },
   "cell_type": "code",
   "source": [
    "model.load_state_dict(torch.load(\"checkpoints/monkeys-resnet50/best.ckpt\", map_location=\"cpu\"))\n",
    "\n",
    "model.eval()\n",
    "loss, acc = evaluating(model, val_loader, loss_fct)\n",
    "print(f\"loss:     {loss:.4f}\\naccuracy: {acc:.4f}\")"
   ],
   "id": "56fb83715d17dac7",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "loss:     0.7939\n",
      "accuracy: 0.9816\n"
     ]
    }
   ],
   "execution_count": 20
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": "",
   "id": "bcce730703983f08"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 2
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython2",
   "version": "2.7.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
